﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using WatiN.Core;
using WatiNCF.Steps;
using AttributeConstraint=WatiN.Core.Constraints.AttributeConstraint;

namespace WatiNCF
{
    public abstract class PageController<TClass>
        : IPageController<TClass>, IHasStepSupport<TClass>
        where TClass : class
    {
        public event EventHandler<StepEventArgs> BeforeStep;
        public event EventHandler<StepEventArgs> AfterStep;

        private Stack<Step> Steps { get; set; }
        protected bool StopExecution { get; set; }
        public IHomePageController HomePage { get; set; }
        public IPageController ParentPage { get; set; }
        
        public IE Browser
        {
            get 
            {
                return HomePage.Browser;
            }
        }

        protected PageController(IHomePageController homePage)
            :this(homePage, null)
        {
        }

        protected PageController(IHomePageController homePage, IPageController parentPage)
        {
            HomePage = homePage;
            ParentPage = parentPage;
            StopExecution = false;
            Steps = new Stack<Step>();
        }

        #region Step Execution
        public void ExecuteStep(Action action)
        {
            BaseExecuteStep(action.Method.Name, action, action);
        }

        public void ExecuteStep<T>(Action<T> action, T arg1)
        {
            BaseExecuteStep(action.Method.Name, action, () => action(arg1));
        }

        public void ExecuteStep<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2)
        {
            BaseExecuteStep(action.Method.Name, action, () => action(arg1, arg2));
        }

        public TResult ExecuteStep<TResult>(Func<TResult> func)
        {
            return BaseExecuteStep(func.Method.Name, func, func, true);;
        }

        public TResult ExecuteStep<T, TResult>(Func<T, TResult> func, T arg1)
        {
            return BaseExecuteStep(func.Method.Name, func, () => func(arg1), true);;
        }

        private void BaseExecuteStep(string stepName, object originalAction, Action callback)
        {
            BaseExecuteStep(stepName, originalAction, callback, true);
        }
        
        private void BaseExecuteStep(string stepName, object originalAction, Action callback, bool recordStep)
        {
            if (!StopExecution)
            {
                StopExecution = FireBeforeStep(stepName);
                if (!StopExecution)
                {
                    try
                    {
                        callback();
                        if (recordStep)
                        {
                            Steps.Push(new ActionStep(this) { Action = callback });
                        }
                        StopExecution = FireAfterStep(stepName);
                    }
                    catch (WatiN.Core.Exceptions.TimeoutException)
                    {
                        LoginIfRequired();
                        BaseExecuteStep(stepName, originalAction, callback, recordStep);
                    }
                }
            }
        }

        private TResult BaseExecuteStep<TResult>(string stepName, object originalFunc, Func<TResult> callback, bool recordStep)
        {
            TResult retVal = default(TResult);
            if (!StopExecution)
            {
                StopExecution = FireBeforeStep(stepName);
                if (!StopExecution)
                {
                    try
                    {
                        retVal = callback();
                        if (recordStep)
                        {
                            Steps.Push(new FuncStep<TResult>(this) { Func = callback });
                        }
                        StopExecution = FireAfterStep(stepName);
                    }
                    catch(WatiN.Core.Exceptions.TimeoutException)
                    {
                        LoginIfRequired();
                        retVal = BaseExecuteStep<TResult>(stepName, originalFunc, callback, recordStep);
                    }
                }
            }
            return retVal;
        }

        private void LoginIfRequired()
        {
            if(this is IRequiresLogin)
            {
                var page = (IRequiresLogin)this;
                if (!page.IsLoggedIn())
                {
                    page.Login();
                }
            }
        }

        public virtual TClass ResetExecutionFlag()
        {
            this.StopExecution = false;
            return this as TClass;
        }

        private bool FireBeforeStep(string stepName)
        {
            var args = new StepEventArgs(stepName);
            if (BeforeStep != null)
            {
                BeforeStep(this, args);
            }
            return args.StopExecution;
        }

        private bool FireAfterStep(string stepName)
        {
            var args = new StepEventArgs(stepName);
            if (AfterStep != null)
            {
                AfterStep(this, args);
            }
            return args.StopExecution;
        }


        /// <summary>
        /// Finishes execution of any further steps before the specified step has started.
        /// </summary>
        /// <param name="stepName">The name of the step to finish after.</param>
        public TClass FinishExecutionBeforeStep(string stepName)
        {
            EnsureStepExists(stepName);
            BeforeStep += (sender, args) =>
            {
                if (args.StepName == stepName)
                {
                    args.StopExecution = true;
                }
            };
            return this as TClass;
        }

        /// <summary>
        /// Finishes execution of any further steps after the specified step has completed.
        /// </summary>
        /// <param name="stepName">The name of the step to finish after.</param>
        /// <returns></returns>
        public TClass FinishExecutionAfterStep(string stepName)
        {
            EnsureStepExists(stepName);
            AfterStep += (sender, args) =>
            {
                if (args.StepName == stepName)
                {
                    args.StopExecution = true;
                }
            };
            return this as TClass;
        }

        

        protected void EnsureStepExists(string stepName)
        {
            IEnumerable<MethodInfo> methods = this.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == stepName);
            if (methods.Count() == 0)
            {
                throw new ApplicationException(string.Format("Step name '{0}' does not exist in {1}", stepName, this.GetType().Name));
            }
        }
        #endregion

        #region WatiN Helpers
        public AttributeConstraint ByPartialId(string partialId)
        {
            return WatinFind.ByPartialId(partialId);
        }

        public void GoToUrl(string url)
        {
            Browser.GoTo(url);
        }
        #endregion        
    }
}
